package com.code.coinprice.servers;

import com.code.coinprice.handlers.HearBeatHandler;
import com.code.coinprice.handlers.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.net.InetSocketAddress;

/**
 * 1、Netty 抽象出两组线程池BossGroup和WorkerGroup，BossGroup专门负责接收客户端的连接, WorkerGroup专
 * 门负责网络的读写
 * 2、BossGroup和WorkerGroup类型都是NioEventLoopGroup
 * 3、NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 ， 每一个事件循环线程是
 * NioEventLoop
 * 4、每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
 * 5、每个Boss NioEventLoop线程内部循环执行的步骤有 3 步
 * 5.1、处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel
 * 5.2、将NioSocketChannel注册到某个worker NIOEventLoop上的selector
 * 5.3、处理任务队列的任务 ， 即runAllTasks
 * 6、 每个worker NIOEventLoop线程循环执行的步骤
 * 6.1、轮询注册到自己selector上的所有NioSocketChannel 的read, write事件
 * 6.2、处理 I/O 事件， 即read , write 事件， 在对应NioSocketChannel 处理业务
 * 6.3、runAllTasks处理任务队列TaskQueue的任务 ，一些耗时的业务处理一般可以放入TaskQueue中慢慢处
 * 理，这样不影响数据在 pipeline 中的流动处理
 * 7、每个worker NIOEventLoop处理NioSocketChannel业务时，会使用 pipeline (管道)，管道中维护了很多 handler
 * 处理器用来处理 channel 中的数据
 */
@Component
public class NettyServer {

    private static final Logger log = LoggerFactory.getLogger(NettyServer.class);

    /**
     * webSocket协议名
     */
    private static final String WEBSOCKET_PROTOCOL = "webSocket";

    /**
     * 端口号默认58080
     */
    @Value("${webSocket.netty.port:58080}")
    private int port;

    /**
     * webSocket路径默认/ws
     */
    @Value("${webSocket.netty.path:/ws}")
    private String webSocketPath;

    @Resource
    private WebSocketHandler webSocketHandler;

    private EventLoopGroup bossGroup;
    private EventLoopGroup workGroup;

    /**
     * 启动
     */
    private void start() throws InterruptedException {
        // 创建两个线程组bossGroup和workerGroup, 含有的子线程NioEventLoop的个数默认为cpu核数的两倍
        // NioEventLoop 中维护了一个线程和任务队列，支持异步提交执行任务，线程启动时会调用 NioEventLoop 的 run 方
        // 法，执行 I/O 任务和非 I/O 任务：
        // I/O 任务，即 selectionKey 中 ready 的事件，如 accept、connect、read、write 等，由 processSelectedKeys 方
        // 法触发。
        // 非 IO 任务，添加到 taskQueue 中的任务，如 register0、bind0 等任务，由 runAllTasks 方法触发。
        // NioEventLoopGroup，主要管理 eventLoop 的生命周期，可以理解为一个线程池，内部维护了一组线程，每个线程
        // (NioEventLoop)负责处理多个 Channel 上的事件，而一个 Channel 只对应于一个线程。
        bossGroup = new NioEventLoopGroup(1);
        workGroup = new NioEventLoopGroup();
        // 创建服务器端的启动对象
        // Bootstrap 意思是引导，一个 Netty 应用通常由一个 Bootstrap 开始，主要作用是配置整个 Netty 程序，串联各个组
        // 件，Netty 中 Bootstrap 类是客户端程序的启动引导类，ServerBootstrap 是服务端启动引导类。
        ServerBootstrap bootstrap = new ServerBootstrap();
        // 使用链式编程来配置参数
        // 设置两个线程组，bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
        bootstrap.group(bossGroup, workGroup)
                // 设置NIO类型的channel，使用NioServerSocketChannel作为服务器的通道实现
                .channel(NioServerSocketChannel.class)
                // 初始化服务器连接队列大小，服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
                // 多个客户端同时来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 设置监听端口
                .localAddress(new InetSocketAddress(port))
                // 连接到达时会创建一个通道
                .childHandler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel ch) {
                        // 流水线管理通道中的处理程序（Handler），用来处理业务
                        // webSocket协议本身是基于http协议的，所以这边也要使用http编解码器
                        ch.pipeline().addLast(new HttpServerCodec());
                        // 当你通过Netty发送或者接受一个消息的时候，就将会发生一次数据转换。入站消息会被解码：从字节转换为另一种格式（比如java对
                        // 象）；如果是出站消息，它会被编码成字节。
                        // Netty提供了一系列实用的编码解码器，他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中，
                        // channelRead方法已经被重写了。以入站为例，对于每个从入站Channel读取的消息，这个方法会被调用。随后，它将调用由已知解码器
                        // 所提供的decode()方法进行解码，并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
                        // Netty提供了很多编解码器，比如编解码字符串的StringEncoder和StringDecoder，编解码对象的ObjectEncoder和ObjectDecoder
                        // 等。
                        ch.pipeline().addLast(new ObjectEncoder());
                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
                        ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                        ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                        // 以块的方式来写的处理器
                        ch.pipeline().addLast(new ChunkedWriteHandler());
                /*
                说明：
                1、http数据在传输过程中是分段的，HttpObjectAggregator可以将多个段聚合
                2、这就是为什么，当浏览器发送大量数据时，就会发送多次http请求
                 */
                        ch.pipeline().addLast(new HttpObjectAggregator(8192));
                /*
                说明：
                1、对应webSocket，它的数据是以帧（frame）的形式传递
                2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
                3、核心功能是将http协议升级为ws协议，保持长连接
                */
                        ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));

                        // 添加Netty空闲超时检查的支持
                        // 1. 读空闲超时（超过一定的时间会发送对应的事件消息）
                        // 2. 写空闲超时
                        // 3. 读写空闲超时
                        ch.pipeline().addLast(new IdleStateHandler(30, 30, 30));
                        //添加心跳处理
                        ch.pipeline().addLast(new HearBeatHandler());
                        // 自定义的handler，处理业务逻辑
                        ch.pipeline().addLast(webSocketHandler);

                    }
                });
        // 配置完成，开始绑定server，通过调用sync同步方法阻塞直到绑定成功
        // 针对Future、ChannelFuture，在 Netty 中所有的 IO 操作都是异步的，不能立刻得知消息是否被正确处理。
        // 但是可以过一会等它执行完成或者直接注册一个监听，具体的实现就是通过 Future 和 ChannelFutures，他们可以注
        // 册一个监听，当操作执行成功或失败时监听会自动触发注册的监听事件。
        // 绑定一个端口并且同步, 生成了一个ChannelFuture异步对象，通过isDone()等方法可以判断异步事件的执行情况
        // 启动服务器(并绑定端口)，bind是异步操作，sync方法是等待异步操作执行完毕
        ChannelFuture channelFuture = bootstrap.bind(port).sync();
        // Channel是Netty 网络通信的组件，能够用于执行网络 I/O 操作。Channel 为用户提供：
        // 1）当前网络连接的通道的状态（例如是否打开？是否已连接？）
        // 2）网络连接的配置参数 （例如接收缓冲区大小）
        // 3）提供异步的网络 I/O 操作(如建立连接，读写，绑定端口)，异步调用意味着任何 I/O 调用都将立即返回，并且不保
        // 证在调用结束时所请求的 I/O 操作已完成。
        // 4）调用立即返回一个 ChannelFuture 实例，通过注册监听器到 ChannelFuture 上，可以 I/O 操作成功、失败或取
        // 消时回调通知调用方。
        // 5）支持关联 I/O 操作与对应的处理程序。
        // 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应，比如：NioSocketChannel是异步的客户端 TCP Socket 连接。
        // NioServerSocketChannel是异步的服务器端 TCP Socket 连接，NioDatagramChannel是异步的 UDP 连接等等
        log.info("netty server - 启动成功:{}", channelFuture.channel().localAddress());
        // 对关闭通道进行监听
        // 对通道关闭进行监听，closeFuture是异步操作，监听通道关闭
        // 通过sync方法同步等待通道关闭处理完毕，这里会阻塞等待通道关闭完成
        //channelFuture.channel().closeFuture().sync();
    }

    /**
     * 释放资源
     *
     * @throws InterruptedException
     */
    @PreDestroy
    public void destroy() throws InterruptedException {
        if (bossGroup != null) {
            bossGroup.shutdownGracefully().sync();
        }
        if (workGroup != null) {
            workGroup.shutdownGracefully().sync();
        }
    }

    @PostConstruct()
    public void init() {
        new Thread(() -> {
            try {
                start();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
